﻿Imports System.Xml

<System.ComponentModel.DesignerCategory("")>
Public Class Sheet

	Public Columns As New List(Of Column)
	Public Rows As New List(Of Row)
	Public WithEvents HScrollBar As New HScrollBar
	Public WithEvents VScrollBar As New VScrollBar

	Sub New()
		DoubleBuffered = True
		InitializeComponent()
		ResizeRedraw = True
		NewSheet()
	End Sub

	Event UndoRedoStatusChanged(Undo As Boolean, Redo As Boolean)

	Property File As String

	ReadOnly Property IsBold As String
		Get
			Return If((From c In Cells.Values Where Sel.Contains(c.x, c.y) Select b = GetCellAttribute("b", c.x, c.y) Where b <> "1").Any, "0", "1")
		End Get
	End Property

	ReadOnly Property IsItalic As String
		Get
			Return If((From c In Cells.Values Where Sel.Contains(c.x, c.y) Select b = GetCellAttribute("i", c.x, c.y) Where b <> "1").Any, "0", "1")
		End Get
	End Property

	Sub NewSheet()
		Rows.Clear()
		Columns.Clear()
		Cells.Clear()
		For i = 0 To 65535
			Dim Row As New Row
			Row.Index = i
			Rows.Add(Row)
		Next
		HScrollBar.Maximum = 255
		VScrollBar.Maximum = 65535
		For i = 0 To 255
			Dim Col As New Column
			Col.Index = i
			Columns.Add(Col)
		Next
		Columns(0).Width = 25
		Rearrange()
		Sel = New Region(Me.Columns.Count, Me.Rows.Count)
		Active = New Point(1, 1)
		Sel.Move(1, 1)
		ClearUndoRedo()
		Refresh()
	End Sub

	Dim _Active As Point = New Point(1, 1)
	Property Active As Point
		Get
			Return _Active
		End Get
		Set(value As Point)
			EndEdit()
			If value.X < 1 OrElse value.Y < 1 Then Return
			Invalidate(DisplayRect(Active.X, Active.Y))
			_Active = value
			Invalidate(DisplayRect(Active.X, Active.Y))
		End Set
	End Property

	Public WithEvents Editor As New CellEditor(Me)

	Protected Overrides Sub OnLoad(e As System.EventArgs)
		MyBase.OnLoad(e)
		'Cursor = New Cursor(New System.IO.MemoryStream(My.Resources.cell))
		Controls.Add(HScrollBar)
		Controls.Add(VScrollBar)
		HScrollBar.Location = New Point(0, Height - HScrollBar.Height)
		HScrollBar.Width = Width - VScrollBar.Width
		VScrollBar.Location = New Point(Width - VScrollBar.Width, 0)
		VScrollBar.Height = Height - HScrollBar.Height
		HScrollBar.Anchor = AnchorStyles.Left + AnchorStyles.Right + AnchorStyles.Bottom
		VScrollBar.Anchor = AnchorStyles.Top + AnchorStyles.Right + AnchorStyles.Bottom
		OnSizeChanged(Nothing)
	End Sub

	Sub EnsureVisible(Optional x As Integer = -1, Optional y As Integer = -1)
		If Not IsHandleCreated Then Return
		If x = -1 Then x = Active.X
		If y = -1 Then y = Active.Y
		Do
			If DisplayRect(x, y).Left >= RF.Left Then Exit Do
			If HScrollBar.Value = 0 Then Exit Do
			HScrollBar.Value -= 1
		Loop
		Do
			If DisplayRect(x, y).Right <= RF.Right Then Exit Do
			HScrollBar.Value += 1
		Loop
		Do
			If DisplayRect(x, y).Top >= RF.Top Then Exit Do
			If VScrollBar.Value = 0 Then Exit Do
			VScrollBar.Value -= 1
		Loop
		Do
			If DisplayRect(x, y).Bottom <= RF.Bottom Then Exit Do
			VScrollBar.Value += 1
		Loop
	End Sub

	ReadOnly Property Editing As Boolean
		Get
			Return Not EditBox Is Nothing AndAlso EditBox.Visible
		End Get
	End Property

	Sub MoveActive(dx As Integer, dy As Integer, Optional MoveSelection As Boolean = True)
		Active = New Point(Active.X + dx, Active.Y + dy)
		If MoveSelection Then Sel.Move(Active.X, Active.Y)
		EnsureVisible()
	End Sub

	Protected Overrides Function ProcessCmdKey(ByRef msg As System.Windows.Forms.Message, keyData As System.Windows.Forms.Keys) As Boolean
		If keyData = Keys.Down Then
			MoveActive(0, 1)
			Return True
		ElseIf keyData = Keys.Up Then
			MoveActive(0, -1)
			Return True
		ElseIf keyData = Keys.Left Then
			MoveActive(-1, 0)
			Return True
		ElseIf keyData = Keys.Right Then
			MoveActive(1, 0)
			Return True
		ElseIf keyData = Keys.Shift + Keys.Down Then
			Sel.OffsetEnd(0, 1)
			'EnsureVisible(cx1, cy1)
			Return True
		ElseIf keyData = Keys.Shift + Keys.Up Then
			Sel.OffsetEnd(0, -1)
			'EnsureVisible(cx1, cy1)
			Return True
		ElseIf keyData = Keys.Shift + Keys.Left Then
			Sel.OffsetEnd(-1, 0)
			'EnsureVisible(cx1, cy1)
			Return True
		ElseIf keyData = Keys.Shift + Keys.Right Then
			Sel.OffsetEnd(1, 0)
			'EnsureVisible(cx1, cy1)
			Return True
		ElseIf keyData = Keys.Enter AndAlso Editing Then
			EndEdit()
			MoveActive(0, 1)
			Return True
		End If
		Return MyBase.ProcessCmdKey(msg, keyData)
	End Function

	Protected Overrides Sub OnPaint(e As System.Windows.Forms.PaintEventArgs)
		MyBase.OnPaint(e)
		If Rows.Count = 0 Or Columns.Count = 0 Then Return
		Dim Format As New StringFormat
		Format.Alignment = StringAlignment.Center
		Format.LineAlignment = StringAlignment.Center
		Dim R0 = DisplayRect(0, 0)
		Dim BorderPen = Color.Black.Opacity(32).Pen
		e.Graphics.FillRectangle(MainColor.Opacity(50).Brush, R0)
		If Sel.AllSelected Then e.Graphics.FillRectangle(SecondaryColor.Brush, R0)
		R0.Width -= 1 : R0.Height -= 1
		e.Graphics.DrawLine(BorderPen, R0.Left, R0.Bottom, R0.Right, R0.Bottom)
		e.Graphics.DrawLine(BorderPen, R0.Right, R0.Bottom, R0.Right, R0.Top)
		For y = VScrollBar.Value + 1 To Rows.Count - 1
			Dim R = DisplayRect(0, y)
			If R.Top > RF.Bottom Then Exit For
			e.Graphics.FillRectangle(MainColor.Opacity(50).Brush, R)
			If Sel.ContainsY(y) Then e.Graphics.FillRectangle(SecondaryColor.Brush, R)
			e.Graphics.DrawString(y, Font, Brushes.Black, R, Format)
			R.Width -= 1 : R.Height -= 1
			e.Graphics.DrawLine(BorderPen, R.Left, R.Bottom, R.Right, R.Bottom)
			e.Graphics.DrawLine(BorderPen, R.Right, R.Bottom, R.Right, R.Top)
		Next y
		For x = HScrollBar.Value + 1 To Columns.Count - 1
			Dim R = DisplayRect(x, 0)
			If R.Left > RF.Right Then Exit For
			e.Graphics.FillRectangle(MainColor.Opacity(50).Brush, R)
			If Sel.ContainsX(x) Then e.Graphics.FillRectangle(SecondaryColor.Brush, R)
			e.Graphics.DrawString(ToLetter(x), Font, Brushes.Black, R, Format)
			R.Width -= 1 : R.Height -= 1
			e.Graphics.DrawLine(BorderPen, R.Left, R.Bottom, R.Right, R.Bottom)
			e.Graphics.DrawLine(BorderPen, R.Right, R.Bottom, R.Right, R.Top - 12)
		Next x
		e.Graphics.Clip = New System.Drawing.Region(RF)
		For y = VScrollBar.Value + 1 To Rows.Count - 1
			If Rows(y).Top - Rows(VScrollBar.Value).Top >= RF.Bottom Then Exit For
			For x = HScrollBar.Value + 1 To Columns.Count - 1
				Dim R = DisplayRect(x, y)
				If R.Left >= RF.Right Then Exit For
				e.Graphics.FillRectangle(GetCellAttribute("bc", x, y).ToColor(Color.White).Brush, R)
				If Not (x = Active.X And y = Active.Y) And Sel.Contains(x, y) Then
					e.Graphics.FillRectangle(SystemColors.Highlight.Opacity(128).Brush, R)
				End If
				R.Width -= 1
				R.Height -= 1
				e.Graphics.DrawLine(BorderPen, R.Left, R.Bottom, R.Right, R.Bottom)
				e.Graphics.DrawLine(BorderPen, R.Right, R.Bottom, R.Right, R.Top)
				Cells.Paint(e, R, x, y, GetCellAttribute("fc", x, y).ToColor(Color.Black), GetCellAttribute("b", x, y), GetCellAttribute("i", x, y))
			Next
		Next
		Dim R1 = Sel.GetArea(Me)
		R1.Offset(-1, -1)
		e.Graphics.DrawRectangle(New Pen(Color.Black, If(Editing, 2, 3)), R1)
	End Sub

	Dim RF As Rectangle

	Protected Overrides Sub OnSizeChanged(e As System.EventArgs)
		MyBase.OnSizeChanged(e)
		If Not IsHandleCreated Then Return
		RF = New Rectangle(Columns(1).Left, Rows(1).Top, Width - VScrollBar.Width - Columns(0).Width, Height - HScrollBar.Height - Rows(0).Height)
		Invalidate()
	End Sub

	Sub Rearrange()
		Dim x As Integer = 0
		For i = 0 To 255
			Columns(i).Left = x
			x += Columns(i).Width
		Next
		Dim y As Integer = 0
		For i = 0 To 65535
			Rows(i).Top = y
			y += Rows(i).Height
		Next
	End Sub

	Private Sub InvalidateCell(x As Integer, y As Integer)
		Dim R = DisplayRect(x, y)
		R.Inflate(2, 2)
		Invalidate(R)
	End Sub

	ReadOnly Property DisplayRect(x As Integer, y As Integer) As Rectangle
		Get
			If Columns.Count = 0 Or Rows.Count = 0 Then Return Rectangle.Empty
			Dim R As New Rectangle(Columns(x).Left, Rows(y).Top, Columns(x).Width, Rows(y).Height)
			R.Offset(If(x = 0, 0, RF.Left - Columns(HScrollBar.Value + 1).Left), If(y = 0, 0, RF.Top - Rows(VScrollBar.Value + 1).Top))
			Return R
		End Get
	End Property

	Function HitText(px As Integer, py As Integer) As Point
		Dim cx = 0, cy = 0
		If px > Columns(0).Width Then
			For x = HScrollBar.Value To Columns.Count - 2
				If px < DisplayRect(x + 1, 0).Left Then cx = x : Exit For
			Next
		End If
		If py > Rows(0).Height Then
			For y = VScrollBar.Value To Rows.Count - 2
				If py < DisplayRect(0, y + 1).Top Then cy = y : Exit For
			Next
		End If
		Return New Point(cx, cy)
	End Function

	Protected Overrides Sub OnMouseDown(e As System.Windows.Forms.MouseEventArgs)
		MyBase.OnMouseDown(e)
		Dim p = HitText(e.X, e.Y)
		If p.X = 0 And p.Y = 0 Then
			Sel.Move(-1, -1)
			Active = New Point(1, 1)
			Return
		ElseIf p.X = 0 Then
			Sel.Move(-1, p.Y)
			Active = New Point(1, p.Y)
			Return
		ElseIf p.Y = 0 Then
			Sel.Move(p.X, -1)
			Active = New Point(p.X, 1)
			Return
		End If
		If Control.ModifierKeys = Keys.Shift Then
			Sel.MoveEnd(p.X, p.Y)
		Else
			Active = New Point(p.X, p.Y)
			Sel.Move(p.X, p.Y)
		End If
	End Sub

	Protected Overrides Sub OnMouseWheel(e As System.Windows.Forms.MouseEventArgs)
		MyBase.OnMouseWheel(e)
		Dim v = VScrollBar.Value - e.Delta / 40
		If v < 0 Then v = 0
		If v > VScrollBar.Maximum Then v = VScrollBar.Maximum
		VScrollBar.Value = v
	End Sub

	Protected Overrides Sub OnMouseMove(e As System.Windows.Forms.MouseEventArgs)
		MyBase.OnMouseMove(e)
		If e.Button = Windows.Forms.MouseButtons.Left Then
			Dim p = HitText(e.X, e.Y)
			Sel.MoveEnd(p.X, p.Y)
		End If
	End Sub

	Protected Overrides Sub OnMouseDoubleClick(e As System.Windows.Forms.MouseEventArgs)
		MyBase.OnMouseDoubleClick(e)
		Dim p = HitText(e.X, e.Y)
		If p.X > 0 And p.Y > 0 Then BeginEdit()
	End Sub

	Protected Overrides Sub OnKeyPress(e As System.Windows.Forms.KeyPressEventArgs)
		MyBase.OnKeyPress(e)
		BeginEdit()
		SendKeys.Send(e.KeyChar)
	End Sub

	Dim WithEvents EditBox As EditBox

	Sub BeginEdit(Optional Focus As Boolean = True)
		If Not EditBox Is Nothing Then Return
		EditBox = New EditBox(Me)
		Select Case Cells(Active.X, Active.Y).FormattedAlignment
			Case TextFormatFlags.Left : EditBox.TextAlign = HorizontalAlignment.Left
			Case TextFormatFlags.HorizontalCenter : EditBox.TextAlign = HorizontalAlignment.Center
			Case TextFormatFlags.Right : EditBox.TextAlign = HorizontalAlignment.Right
		End Select
		EditBox.ForeColor = GetCellAttribute("fc", Active.X, Active.Y).ToColor(Color.Black)
		EditBox.BackColor = GetCellAttribute("bc", Active.X, Active.Y).ToColor(Color.White)
		EditBox.Twin = Editor.EditBox
		Editor.EditBox.Twin = EditBox
		Dim R = DisplayRect(Active.X, Active.Y)
		R.Inflate(-4, -1)
		EditBox.Location = R.Location
		EditBox.Size = R.Size
		Controls.Add(EditBox)
		EditBox.SetValue(Cells(Active.X, Active.Y).Value)
		EditBox.SelectAll()
		EditBox.Visible = True
		If Focus Then EditBox.Focus()
		InvalidateCell(Active.X, Active.Y)
	End Sub

	Public Cells As New Cells(Me)

	Sub EndEdit()
		If Not Editing Then Return
		Cells(Active.X, Active.Y).Value = EditBox.Text
		Me.Focus()
		Editor.EditBox.Twin = Nothing
		Controls.Remove(EditBox)
		EditBox = Nothing
		InvalidateCell(Active.X, Active.Y)
	End Sub

	Private Sub ScrollBar_ValueChanged(sender As Object, e As System.EventArgs) Handles HScrollBar.ValueChanged, VScrollBar.ValueChanged
		Invalidate()
	End Sub

	Dim WithEvents Sel As Region

	Private Sub InvalidateSelection()
		Dim R = Sel.GetArea(Me)
		R.Inflate(3, 3)
		Invalidate(R)
		R = Sel.GetAreaX(Me)
		R.Inflate(3, 3)
		Invalidate(R)
		R = Sel.GetAreaY(Me)
		R.Inflate(3, 3)
		Invalidate(R)
		Invalidate(DisplayRect(0, 0))
		Editor.SetValue(Cells(Active.X, Active.Y).Value)
	End Sub

	Private Function ToLetter(x As Integer) As String
		Dim a1 = Int((x - 1) / 26)
		Dim b1 = (x - 1) Mod 26
		Return If(a1 = 0, "", Chr(64 + a1)) & Chr(65 + b1)
	End Function

	Sub Save()
		Dim Doc As New XmlDocument
		Dim Root = Doc.AppendChild(Doc.CreateElement("Root"))
		Dim Node As XmlElement = Root.AppendChild(Doc.CreateElement("Cells"))
		For Each Cell In Cells.Values
			Cell.Save(Doc, Node)
		Next
		Node = Root.AppendChild(Doc.CreateElement("Regions"))
		For Each R In Regions
			R.Save(Doc, Node)
		Next
		Doc.Save(File)
	End Sub

	Sub Open()
		Dim Doc As New XmlDocument
		Doc.Load(File)
		Dim Root = Doc.GetElementsByTagName("Root")(0)
		Dim Parent = Doc.GetElementsByTagName("Cells")(0)
		For Each Node As XmlElement In Parent.ChildNodes
			With Cells(Node.GetAttribute("x"), Node.GetAttribute("y"))
				.Value = Node.GetAttribute("Text")
			End With
		Next
		Parent = Doc.GetElementsByTagName("Regions")(0)
		If Not Parent Is Nothing Then
			For Each Node As XmlElement In Parent.ChildNodes
				Regions.Add(New Region(Node))
			Next
		End If
		ClearUndoRedo()
		Invalidate()
	End Sub

	Sub ClearUndoRedo()
		RaiseEvent UndoRedoStatusChanged(False, False)
	End Sub

	Sub Paste()
		If Clipboard.ContainsText Then
			Dim y = Active.Y
			For Each Line In Split(Clipboard.GetText, vbCrLf)
				Dim x = Active.X
				For Each Value In Split(Line, vbTab)
					Cells(x, y).Value = Value
					x += 1
				Next
				y += 1
			Next
		End If
	End Sub

	Sub ClearContents()
		For Each Cell In From c In Cells.Values Where Sel.Contains(c.x, c.y)
			Cell.Value = ""
		Next Cell
	End Sub

	Sub SelectAll()
		Sel.Move(-1, -1)
	End Sub

	'Dim BackColors As New List(Of Region)
	'Dim ForeColors As New List(Of Region)
	Dim Regions As New List(Of Region)

	Sub AddRegion(Type As String, Tag As String)
		For Each Item In (From rg In Regions Where rg.Type = Type AndAlso Sel.Contains(rg)).ToList
			Regions.Remove(Item)
		Next
		Dim R = Sel.Clone
		R.Type = Type
		R.Tag = Tag
		Regions.Add(R)
		InvalidateSelection()
	End Sub

	Private Function GetCellAttribute(Type As String, x As Integer, y As Integer) As String
		Return (From r In Regions Where r.Type = Type AndAlso r.Contains(x, y) Select r.Tag).LastOrDefault
	End Function

	Private Sub Sel_Change() Handles Sel.Change
		InvalidateSelection()
	End Sub

End Class







Public Class Column
	Public Index As Integer
	Public Width As Integer = 72
	Public Left As Integer
End Class

Public Class Row
	Public Index As Integer
	Public Height As Integer = 20
	Public Top As Integer
End Class




